package hn.sigit.util;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFactorySpi;
import org.geotools.data.DataStoreFinder;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.FeatureSource;
import org.geotools.data.FeatureStore;
import org.geotools.data.Transaction;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureCollections;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;

import com.vividsolutions.jts.geom.Geometry;

/**
 * Represents a shape file (used by ArcGIS). Basically a convenience function to simplify the
 * tasks of reading/writing shapefiles (using GeoTools). Most of the shapefile reading/writing/editing 
 * code has been copied or adapted from the GeoTools tutorials which come with the GeoToolssoftware.
 * <p>
 * Typical usage (reads a shapefile, get an attribute, adds a new attribute column, then writes it):<br>
 * <code>
 * Shapefile s = new Shapefile("ObjectIDs", "myShapefile.shp");<br>
 * s.readShapefile();<br>
 * int anAttributeValue = s.getAttribute(featureID, "anAttribute", Integer.class);<br>
 * s.addAttribute("newAttribute", Double.class, valuesTable)
 * s.writeShapefile("newShapefile");
 * </code>
 * 
 */
public class ShapeFile {

	/** The geometries of the objects in this Shapefile, stored along with object ids.*/
	private Map<String, Geometry> geometries;

	/** The features associated with each object in this Shapefile, stored along with object ids. */
	private Map<String, SimpleFeature> features;

	/** The actual file which this shapefile has been created from */
	private File file;

	/** The FeatureSource for this shapefile, required as a template for writing shapefiles */
	private FeatureSource<SimpleFeatureType, SimpleFeature> featureSource;

	/** Optional column used to store object ids (by default will use FID). */
	private String idColumn = null;


	/**
	 * Create a new Shapefile object.
	 * @param idColumn
	 * @param file
	 */
	public ShapeFile(String idColumn, File file) {
		this.geometries = new Hashtable<String, Geometry>();
		this.features = new Hashtable<String, SimpleFeature>();
		this.idColumn = idColumn;
		this.file = file;
	}

	/**
	 * Get the value of an attribute.
	 * @param featureID The ID of the object to get the attribute value from.
	 * @param attributeName The name of the attribute.
	 * @param clazz The class of the obect to be returned.
	 * @return The attribute as an Object or 
	 */
	@SuppressWarnings("unchecked")
	public <T> T getAttribute(int featureID, String attributeName, Class<T> clazz) {
		if (this.checkFeatures()) { // Check there are some features in this Shapefile
			SimpleFeature feature = this.features.get(featureID);
			if (feature == null) {
				System.err.println("Shapefile.getAttribute() error: no feature with id: "+featureID+
				" in this Shapefile");
				return null;
			}
			Object attribute = feature.getAttribute(attributeName);
			T a = null; // The attribute cast to it's correct class.
			if (clazz.isAssignableFrom(clazz)) {
				a = (T) attribute;
			}
			else {
				System.err.println("The attribute "+attributeName+" cannot be assigned to a "+
						clazz.getName()+", it is a: "+attribute.getClass().getName());
			}
			return a;
		}
		else { // No features in this Shapefile
			return null;
		}
	}

	/**
	 * Add a new attribute to this Shapefile.
	 * @param attributeName The name of the attribute to add.
	 * @param valuesClass The class of the values to be added.
	 * @param attributeValues A map of the IDs of each feature and the associated value of the
	 * new attribute
	 * @return True if the operation was successful, false otherwise.
	 */
	public boolean addAttribute(String attributeName, Class<?> valuesClass, Map<Integer, Object> attributeValues) {
		if (!this.checkFeatures()) {
			return false;
		}
		// Check the input values are ok
		List<Integer> badIDs = new ArrayList<Integer>();
		for (Integer i:attributeValues.keySet())
			if (!this.features.containsKey(i))
				badIDs.add(i);
		if (badIDs.size()>0) {
			System.err.println("Shapefile.addAttribute() error: could not find features associated " +
					"with the following IDs: "+badIDs.toString());
			return false;
		} // if badIDs
		/* Method works be re-building all features with the new attributed added */
		// Create a feature type builder to create new features with the added attribute
		SimpleFeatureTypeBuilder featureTypeBuilder = new SimpleFeatureTypeBuilder();
		// Use first feature to initialise the feature builder
		featureTypeBuilder.init(features.values().iterator().next().getFeatureType());
		// Add the new attribute
		featureTypeBuilder.add(attributeName, valuesClass);
		// Create a feature builder to create the new features
		SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureTypeBuilder.buildFeatureType());
		// Iterate over all existing features, creating new ones with the added attribute
		Map<String, SimpleFeature> newFeatures = new Hashtable<String, SimpleFeature>();
		for (String id:this.features.keySet()) {
			SimpleFeature newFeature = featureBuilder.buildFeature(id);
			SimpleFeature existingFeature = this.features.get(id);
			// Add all existing attributes to the new feature
			for (int i = 0; i < existingFeature.getAttributeCount(); i++) {
				newFeature.setAttribute(i, existingFeature.getAttribute(i));
			}
			// Add the new attribute to the new feature
			newFeature.setAttribute(attributeName, attributeValues.get(id));
			// Replace the existing feature with the new one
			newFeatures.put(id, newFeature);
		}
		// Finally replace all old features with the old ones
		for (String id:this.features.keySet()) {
			this.features.put(id, newFeatures.get(id));
		}
		return true;
	}

	/** Make sure there are some features in this Shapefile, return fals if not. */
	private boolean checkFeatures() {
		if (this.features==null || this.features.size()==0)
			return false;
		else
			return true;
	}

	/**
	 * Read in all objects stored in the shapefile, adding them to this Shapefile object.
	 * @return true if everything was read in successfully, false otherwise.
	 */
	public boolean readShapefile() {

		this.clear(); // Clear all objects from this Shapefile

		// Connection to the shapefile
		Map<String, Serializable> connectParameters = new HashMap<String, Serializable>();

		try {
			connectParameters.put("url", this.file.toURI().toURL()/*"file://C:/Users/Hola/Documents/puntos.shp"*/);
			//connectParameters.put("create spatial index", true);
			DataStore dataStore = DataStoreFinder.getDataStore(connectParameters);

			// we are now connected
			String[] typeNames = dataStore.getTypeNames();
			String typeName = typeNames[0];

			this.featureSource = dataStore.getFeatureSource(typeName); 
			FeatureCollection<SimpleFeatureType, SimpleFeature> collection = featureSource.getFeatures();
			FeatureIterator<SimpleFeature> iterator = collection.features();

			try {
				while (iterator.hasNext()) {
					SimpleFeature feature = iterator.next();
					// Set the feature's ID
					String id = "0";
					try {
						if (this.idColumn!=null)
							id = (String) feature.getAttribute(this.idColumn);
						else
							id = feature.getID();
					}
					catch (ClassCastException e) {
						System.err.println("Shapfile.readObjects() error: cannot read integer ids from the " +
								"column: "+this.idColumn+". Check this column stores unique integer IDs.");
						return false;
					}
					catch (NullPointerException e) {
						System.err.println("Shapfile.readObjects() error: cannot read integer ids from the" +
								"column: "+this.idColumn+". Check this column stores unique integer IDs.");
						return false;
					}
					catch (NumberFormatException e) {
						System.err.println("Shapfile.readObjects() error: cannot cast this feature's " +
						"ID to an integer. (?)");
						return false;
					}

					if (features.containsKey(id)) {
						System.err.println("Shapefile.readObjects() error: this feature's ID ("+id
								+")is not unique ");
						return false;
					}
					features.put(id, feature);
					Geometry geometry = (Geometry) feature.getDefaultGeometry();
					geometries.put(id, geometry);                    
				} // while iterator.hasNext()
			} finally {
				if (iterator != null) {
					iterator.close();
				}
			}
		} catch (MalformedURLException e) {
			e.printStackTrace();
			return false;
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
		return true;
	} // readShapefile

	/**
	 * Write out all the features stored in this Shapefile to a shapefile.
	 * @param outputFileName The name of the shapefile to write to.
	 * @return True if the operation was a success, false otherwise.
	 */
	public boolean writeShapefile(String outputFileName) {
		if (!this.checkFeatures()) {
			return false;
		}
		// Check the output file can be written to
		File outFile = new File(outputFileName);
//		if (!outFile.canWrite()) {
//			System.err.println("Shapefile.writeShapefile() error: cannot write to the shapefile: "+outputFileName);
//			return false;
//		}		
		// Create a feature collection to write the features out to
		FeatureCollection<SimpleFeatureType, SimpleFeature> outFeatures = FeatureCollections.newCollection();
		for (SimpleFeature f:this.features.values()) {
			outFeatures.add(f);
		}
		try {
			// Don't really get the rest, copied from the GeoTools tutorial.
			DataStoreFactorySpi factory = new ShapefileDataStoreFactory();
			Map<String, Serializable> create = new HashMap<String, Serializable>();
			create.put("url", outFile.toURI().toURL());
			create.put("create spatial index", Boolean.TRUE);
			ShapefileDataStore newDataStore = (ShapefileDataStore) factory.createNewDataStore(create);
			newDataStore.createSchema(outFeatures.getSchema());
			Transaction transaction = new DefaultTransaction("create");
			String typeName = newDataStore.getTypeNames()[0];
			FeatureStore<SimpleFeatureType, SimpleFeature> featureStore;
			featureStore = (FeatureStore<SimpleFeatureType, SimpleFeature>) newDataStore.getFeatureSource(typeName);
			featureStore.setTransaction(transaction);
			try {
				featureStore.addFeatures(outFeatures);
				transaction.commit();
			} catch (Exception problem) {
				System.out.println("Shapefile.writeShapefile() caught a problem trying to write: "+problem.toString());
				problem.printStackTrace();
				transaction.rollback();
				return false;
			} finally {
				transaction.close();
			}

		} catch (IOException e) {
			System.err.println("Shapefile.writeShapefile() caught an IOException trying to write shapefile.");
			e.printStackTrace();
			return false;
		}
		return true;
	}

	/**
	 * Clears all objects from this Shapefile, useful for when the shape file is going to be re-read.
	 */
	private void clear() {
		this.geometries.clear();
		this.features.clear();
	}
	
	/**
	 * Resets this <code>Shapefile</code> by removing all existing features and re-reading the original
	 * shapefile.
	 */
	public void reset() {
		this.clear();
		this.readShapefile();
	}
	
	/**
	 * Return the ID's of all the features in this shapfile. This can be used to iterate over all
	 * <code>SimpleFeature</code>s or all <code>Geometry</code>s.
	 * @return The ID of every feature currently in this <code>Shapefile</code>.
	 */
	public Set<String> getFeatureIDs() {
		return this.features.keySet();
	}
	
	/**
	 * Get the feature with the associated ID.
	 * @param id
	 * @return The Feature or null if no feature is found.
	 */
	public SimpleFeature getFeature(int id) {
		if (!this.features.containsKey(id)) {
			System.out.println("Shapefile.getFeature() error, no feature found with ID: "+id);
		}
		return this.features.get(id);
	}
	public Collection<SimpleFeature> getFeatures() {
		return Collections.unmodifiableCollection(this.features.values());
	}
	
	/**
	 * Get the geometry of the object with the associated ID.
	 * @param id
	 * @return The Geometry or null if no feature is found.
	 */
	public Geometry getGeometry(int id) {
		if (!this.geometries.containsKey(id)) {
			System.out.println("Shapefile.getFeature() error, no feature found with ID: "+id);
		}
		return this.geometries.get(id);
	}
	public Collection<Geometry> getGeometries() {
		return Collections.unmodifiableCollection(this.geometries.values());
	}
}
